feat(ppvm-vihaco): add the composite machine and VM#174
Conversation
Add the leaf `vihaco-circuit-isa` crate — the `CircuitInstruction` enum plus its `CircuitEffect`/`CircuitMessage` companions — as a pure workspace addition. Snapshotted from the tip of `david/42-circuit-component` (PR #168) so it carries the public `vihaco 0.1.1` dependencies rather than the crate's early local-path/`0.1.0` history. The crate is a leaf: nothing on this branch depends on it yet, so it cannot regress existing code. It is the base of the circuit-component split (see #168): merge order is core rotations -> Pauli trace/reset -> this crate -> circuit component -> composite -> CLI/TUI (#166). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Introduce the `ppvm-vihaco` crate with just the circuit component slice: - `component.rs` — the `Circuit` component: dispatches `vihaco_circuit_isa::CircuitInstruction`s to a tableau or PauliSum backend, emitting measurement / trace effects (includes the R gate and trace/reset dispatch now that RotXY and tableau expectation/reset are on `main`). - `measurements.rs` — measurement / trace effect + observer types. - `device_info.rs` — `PPVM_MAGIC`, `BackendKind`, `PPVMDeviceInfo`, relocated out of the composite so `component` compiles without the composite machine (which lands in a follow-up PR). - `lib.rs` — module wiring for the component-only slice; the composite-driven helpers (load/run/dump/parse) come with the composite. `chumsky` / `vihaco-parser-core` are transitive through the `vihaco_parser::Parse` derive on `BackendKind`, so they carry a machete ignore. Includes standalone smoke tests for the tableau backend (single-qubit flip and CNOT propagation via the component's instruction dispatch); the fuller integration coverage arrives with the composite's `.sst` fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the `ppvm-vihaco` crate on top of the circuit component:
- `composite.rs` — the `PPVM` composite: the vihaco machine (CPU + program
loader) driving the circuit component, measurement/trace observers, backend
selection from the device header, resets, and single-instruction stepping.
Imports `BackendKind` / `PPVMDeviceInfo` / `PPVM_MAGIC` from `device_info`
and re-exports them so existing `crate::composite::{…}` paths keep resolving.
- `syntax.rs` — `.sst` header parsing / lowering (`PPVMHeader`, `PPVMResolver`).
- `bytecode.rs` — `.ssb` bytecode emission / loading (magic-tagged).
- `observable.rs` — observable-header parsing for the PauliSum backends.
- `shots.rs` — multi-shot execution (serial + optional rayon parallelism).
- `lib.rs` — full module wiring plus the `load`/`run`/`dump`/`compile`/`parse`
helpers and `PPVMModule` type.
- `Cargo.toml` — adds `vihaco-cpu`, `log`, and the optional `rayon` feature.
Includes the `.sst` integration fixtures (bell, GHZ, function calls, branching,
trotter truncation, loss/trace, rotations) and the composite half of the
PauliSum reset.
Tests: `cargo test -p ppvm-vihaco` — 111 unit + 28 integration; green with and
without `--features rayon`. clippy + machete clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
Pull request overview
This PR completes the ppvm-vihaco crate by adding the composite PPVM machine/VM layer on top of the existing circuit component, plus end-to-end tooling (text syntax, bytecode format, observable parsing, and multi-shot execution) and a substantial .sst fixture suite to exercise the public API.
Changes:
- Introduces the
composite::PPVMmachine wiring (vihaco-cpu+ circuit component + observers) with stepping, file/program loading, and REPL-style single-instruction execution. - Adds
.sstparsing/lowering (syntax.rs) and.ssbbytecode framing with magic/version/device-info (bytecode.rs), plus observable-header parsing (observable.rs). - Adds shot runner utilities (
shots.rs) and 28-ish end-to-end.sstfixtures with an integration harness (sst_fixtures.rs).
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/ppvm-vihaco/src/composite.rs | New composite PPVM machine: loader+CPU+circuit, stepping/run, observers, REPL instruction injection, device init. |
| crates/ppvm-vihaco/src/syntax.rs | .sst header + instruction parsing and module resolution (labels/branches/calls, string table lowering). |
| crates/ppvm-vihaco/src/bytecode.rs | .ssb container format read/write for resolved modules (magic/version/device header + strings + code). |
| crates/ppvm-vihaco/src/observable.rs | Parser for device circuit.observable into Pauli-sum terms used by PauliSum backends. |
| crates/ppvm-vihaco/src/shots.rs | Serial/parallel multi-shot execution API and deterministic per-shot seeding. |
| crates/ppvm-vihaco/src/lib.rs | Public module wiring and helper APIs (parse/compile/load/run/dump) plus PPVMModule alias. |
| crates/ppvm-vihaco/Cargo.toml | Adds vihaco-cpu, log, optional rayon feature, and dependency wiring. |
| Cargo.lock | Locks new deps (vihaco-cpu, log, rayon) into the workspace lockfile. |
| crates/ppvm-vihaco/tests/sst_fixtures.rs | Integration harness that runs .sst fixtures (and dump→load round-trips) via public PPVM APIs. |
| crates/ppvm-vihaco/tests/bell.sst | Fixture: Bell prep + measurements. |
| crates/ppvm-vihaco/tests/hello_circuit.sst | Fixture: small gate sequence without measurement. |
| crates/ppvm-vihaco/tests/rotxy.sst | Fixture: circuit.r end-to-end (rotxy) + measurement. |
| crates/ppvm-vihaco/tests/function_call.sst | Fixture: call into helper and return value plumbing (basic). |
| crates/ppvm-vihaco/tests/function_call_ret.sst | Fixture: ret 1-style return-value workflow and branching in caller. |
| crates/ppvm-vihaco/tests/function_call_branch_both.sst | Fixture: tri-state outcome branching (incl. loss) with helper returning an outcome. |
| crates/ppvm-vihaco/tests/branch_on_outcome.sst | Fixture: probabilistic branch steering with invariant check. |
| crates/ppvm-vihaco/tests/branch_on_outcome_x.sst | Fixture: deterministic branch steering. |
| crates/ppvm-vihaco/tests/paulisum_bell_trace.sst | Fixture: PauliSum backend Bell ⟨ZZ⟩ trace via Heisenberg order. |
| crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst | Fixture: PauliSum backend GHZ ⟨XXX⟩ trace via Heisenberg order. |
| crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst | Fixture: PauliSum backend non-Clifford RY trace behavior. |
| crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst | Fixture: PauliSum backend multi-term observable seeding + trace. |
| crates/ppvm-vihaco/tests/paulisum_measure_error.sst | Fixture: PauliSum backend measure rejection behavior. |
| crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst | Fixture: PauliSum Trotter + explicit truncate between layers. |
| crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst | Fixture: LossyPauliSum loss-channel + trace. |
| crates/ppvm-vihaco/tests/tableau_bell_trace.sst | Fixture: Tableau backend trace against positional Pauli word. |
| crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst | Fixture: Tableau backend GHZ trace. |
| crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst | Fixture: Tableau backend non-Clifford RY then trace. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Term: coefficient [*] word | bare word. | ||
| let term_with_coeff = coefficient | ||
| .then_ignore(just('*').padded().or_not()) | ||
| .then(pauli_word) | ||
| .map(|(c, w)| (w, c)); |
| /// Append one REPL command's lowered VM ops and run just that block against | ||
| /// the persistent state, advancing the pc through it. NOTE: an out-of-range | ||
| /// qubit index *panics* in the tableau rather than erroring, so callers must | ||
| /// bounds-check qubit operands against `n_qubits` first. | ||
| pub fn execute_single_instruction(&mut self, instrs: &[PPVMInstruction]) -> eyre::Result<()> { | ||
| let start = self.loader.module.code.len() as u32; | ||
| self.loader.module.code.extend_from_slice(instrs); | ||
| *self.loader.pc_mut() = start; | ||
| for _ in 0..instrs.len() { | ||
| self.step_once()?; | ||
| } | ||
| Ok(()) | ||
| } |
| // Don't pre-allocate from an untrusted count; grow as entries are read. | ||
| let string_count = read_u32(r)?; | ||
| let mut strings = Vec::new(); | ||
| for _ in 0..string_count { | ||
| let len = read_u32(r)? as usize; | ||
| let mut bytes = vec![0u8; len]; | ||
| r.read_exact(&mut bytes)?; | ||
| strings.push(String::from_utf8(bytes)?); | ||
| } | ||
|
|
||
| let code_count = read_u32(r)?; | ||
| let mut code = Vec::new(); | ||
| for _ in 0..code_count { | ||
| code.push(PPVMInstruction::from_bytes(r)?); | ||
| } |
| [features] | ||
| # Enables shot-level parallelism in `run_shots_parallel`/`run_shots`. | ||
| rayon = ["dep:rayon"] | ||
|
|
||
| [dependencies] |
| // TODO: aspirational — depends on `ret <n>` restoring the caller's PC and | ||
| // leaving the top `n` values on the caller's stack. The runtime doesn't do | ||
| // this today (Frame has no return_pc), so this fixture currently fails. |
The size-bucketed circuit constructors panicked when n_qubits exceeded the widest executor bucket. Since n_qubits comes from user-supplied device headers and this backend drives a TUI, a panic would tear down the terminal rather than surface a friendly message. Return eyre::Result from the four constructor pairs (Tableau/PauliSum/LossyPauliSum, plus new_with_seed) so callers can report the error instead of aborting. Add a MAX_QUBITS constant so the ceiling is single-sourced across the bucket checks and error messages. Default stays infallible (0 qubits always fits the smallest bucket). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…init_inner The circuit constructors now return eyre::Result (merged from #173), so PPVM::init_inner propagates with `?`. It already returns eyre::Result<()> and reports device-header errors, so this just flows the construction error (e.g. n_qubits > MAX_QUBITS) into the same path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The documented observable grammar allows `coefficient WS* pauli_word`, but the parser only tolerated whitespace around an explicit `*`, so "0.5 ZZ" failed to parse. Consume optional whitespace before the Pauli word to match the grammar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously ppvm-tableau's rayon was force-enabled on every non-wasm build via a target-specific dependency, so downstream could not disable parallelism and the `rayon` feature only gated shot-level parallelism. Fold both into one off-by-default `rayon` feature (dep:rayon + ppvm-tableau/rayon) and drop the forced target override. Consumers opt in for parallelism (ppvm-cli already does); off by default keeps the default build wasm-safe with no CI changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| pub fn module_to_bytes(module: &PPVMModule) -> eyre::Result<Vec<u8>> { | ||
| let mut buf = Vec::new(); | ||
| write_module(module, &mut buf)?; | ||
| Ok(buf) | ||
| } |
| } | ||
|
|
||
| /// Reconstruct a module from a byte slice. | ||
| pub fn module_from_bytes(bytes: &[u8]) -> eyre::Result<PPVMModule> { |
There was a problem hiding this comment.
prob should move to vihaco as part of the public interface of Module too
cc: @robpatterson13
|
|
||
| measurement_record: MeasurementObserver, | ||
|
|
||
| trace_record: TraceObserver, |
There was a problem hiding this comment.
FYI. this is where the deq component needs to go eventually
cc: @rafaelha
| fn pop_qubit(&mut self) -> eyre::Result<usize> { | ||
| match self.cpu.stack_pop()? { | ||
| vihaco::Value::U32(v) => Ok(v as usize), | ||
| vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into), |
There was a problem hiding this comment.
maybe restrict the qubit address to u64 - technically I64 is illegal input to the instructions otherwise we will be making this weak typed.
| fn continue_effects(&mut self, effects: Effects<PPVMEffect>) -> eyre::Result<StepOutcome> { | ||
| let mut step_outcome = None; | ||
| for effect in effects { | ||
| match effect { | ||
| PPVMEffect::Step(outcome) => { | ||
| if step_outcome.replace(outcome).is_some() { | ||
| return Err(eyre::eyre!( | ||
| "expected exactly one PPVM step effect, got multiple" | ||
| )); | ||
| } | ||
| } | ||
| effect => self.continue_observer_effect(effect)?, | ||
| } | ||
| } | ||
|
|
||
| step_outcome.ok_or_else(|| eyre::eyre!("expected exactly one PPVM step effect, got 0")) | ||
| } |
There was a problem hiding this comment.
this looks a bit redundant to me, maybe worth something to think about simplifying upstream?
cc: @robpatterson13
| pub fn load_bytecode(&mut self, bytes: &[u8]) -> eyre::Result<()> { | ||
| let module = crate::bytecode::module_from_bytes(bytes)?; | ||
| self.load(&module) | ||
| } | ||
|
|
||
| /// Read a `.ssb` file and load the module it contains. | ||
| pub fn load_bytecode_file(&mut self, path: &str) -> eyre::Result<()> { | ||
| let bytes = std::fs::read(path)?; | ||
| self.load_bytecode(&bytes) | ||
| } | ||
|
|
||
| pub fn run_program(&mut self, program: &str) -> eyre::Result<()> { | ||
| self.load_program(program)?; | ||
| self.run()?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn run_file(&mut self, path: &str) -> eyre::Result<()> { | ||
| self.load_file(path)?; | ||
| self.run()?; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
I think these are also some helper traits worth considering upstream
cc: @robpatterson13
| @@ -1,19 +1,118 @@ | |||
| // SPDX-FileCopyrightText: 2026 The PPVM Authors | |||
There was a problem hiding this comment.
I think helper functions in this file are also worth upstreaming to provide
cc: @robpatterson13
| #[token = "circuit.observable"] | ||
| #[delimiters(open = "", close = "", separator = "")] | ||
| Observable(#[parse_with = "vihaco_parser_core::ident"] String), | ||
|
|
| for _ in 0..string_count { | ||
| let len = read_u32(r)? as usize; | ||
| let mut bytes = vec![0u8; len]; | ||
| r.read_exact(&mut bytes)?; | ||
| strings.push(String::from_utf8(bytes)?); | ||
| } |
| use vihaco::frame::Frame; | ||
| use vihaco::machine::StackFrame; | ||
| use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; | ||
| use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; | ||
| use vihaco::{Effects, Observe, ProgramLoader, Value, composite, observe}; |
| const.u64 0 | ||
| circuit.measure | ||
|
|
||
| const.u64 0 |
| // Jump into the helper, which finishes the program with `halt`. | ||
| // Using `halt` instead of `ret` from the callee avoids depending on | ||
| // vihaco-cpu restoring a return PC, which it doesn't track today. |
| // `function_call.sst` has main `call` into `@run_circuit`, which puts q1 | ||
| // in |+>, measures it, and `halt`s. Verifies CallPatch resolves the | ||
| // symbolic target and op_call actually transfers control there. |
## Merge order Part of the #168 split. This PR targets **#174** (`david/42.3-composite`) and adds the CLI binary the TUI PR builds on: 1. RotXY / R gate (#170) → `main` ✅ merged 2. tableau expectation/reset (#172) → `main` ✅ merged 3. vihaco-circuit-isa (#169) → `main` (open) 4. native `.pyi` stubs (#171) → `main` (open; independent) 5. circuit component (#173) → #169 6. composite machine + VM (#174) → #173 7. **ppvm-cli binary (this PR)** → #174 8. ratatui TUI (#166) → this PR ## Summary Extracts the `ppvm-cli` crate — the `ppvm` binary — from #168 onto the composite. This is the **base CLI without the TUI**: clap-based `run` / `dump` / `parse` / `debug` subcommands over `ppvm-vihaco`, plus example `.sst` programs. The ratatui TUI that layers on top of it is the follow-up PR (#166), which modifies this crate. `ppvm-cli` was part of #168's implementation but had no extraction step of its own — the original split plan conflated it with the TUI PR. It slots in here between the composite and the TUI. - Depends on `ppvm-vihaco` (with `rayon`), `clap`, `eyre`. - **Excluded from the wasm32 CI build** (terminal binary, forces `rayon`), mirroring #168's exclusion — `ppvm-vihaco` itself still builds for wasm, so only `ppvm-cli` is excluded; the reusable engine stays covered. ## Testing `cargo test -p ppvm-cli` — 23 pass. `ppvm run examples/ghz.sst` runs. clippy + machete clean; full workspace builds; `ppvm-vihaco` still builds for `wasm32-unknown-unknown`. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Merge order
Part of the #168 split. This PR targets #173 (
david/42.2-circuit-component) and sits at the top of the vihaco line:main✅ mergedmain✅ mergedmain(open; carries feat(gates): add RotXY in-plane single-qubit rotation (R gate) #170 + feat(tableau): add Pauli expectation/trace and reset_all #172 via amainmerge).pyistubs (feat(python-native): add type stubs for the native _core module #171) →main(open; independent)Summary
Completes the
ppvm-vihacocrate on top of the circuit component (#173) — the composite machine that drives it plus the surrounding VM.composite.rsPPVMcomposite: vihaco machine (CPU + program loader) driving the circuit component, measurement/trace observers, backend selection from the device header, reset, single-instruction steppingsyntax.rs.sstheader parse / lower (PPVMHeader,PPVMResolver)bytecode.rs.ssbbytecode emit / load (magic-tagged)observable.rsshots.rsrayon)lib.rsload/run/dump/compile/parsehelpers +PPVMModuleCargo.tomlvihaco-cpu,log, optionalrayonfeaturetests/*.sst+sst_fixtures.rsdevice_inforelocation resolves herecomposite.rsno longer definesPPVM_MAGIC/BackendKind/PPVMDeviceInfo— it imports them fromdevice_info(introduced in #173) andpub use-re-exports them, so the existingcrate::composite::{…}paths inlib.rs/bytecode.rs/syntax.rskeep resolving with no other edits.The machete-ignore added in #173 is dropped here — the full crate uses
chumsky/vihaco-parser-coredirectly.Testing
cargo test -p ppvm-vihaco— 111 unit + 28 integration tests, green with and without--features rayon. Full workspace builds; clippy + machete clean.